iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 9
2
自我挑戰組

30天學python系列 第 9

[Day09] 物件導向進階

  • 分享至 

  • xImage
  •  

@property

將屬性命名以單下劃線開頭,利用這種方式來暗示屬性是受保護的,不建議外界直接訪問。如果想訪問屬性可以透過屬性的 getter(訪問器)和 setter(修改器)方法進行操作。可以使用 @property 來包裝 getter 和 setter 方法,使得屬性的訪問安全又方便。

class Person(object):

    def __init__ (self, name, age):
        self._name = name
        self._age = age

    # getter
    @property
    def name(self):
        return self._name

    # getter
    @property
    def age(self):
        return self._age
        
    # setter
    @age.setter
    def age(self, age):
        self._age = age

    def play(self):
        if self._age <= 16:
            print('%s 正在玩手機' % self._name)
        else:
            print('%s 正在玩電腦' % self._name)

def main():
    person = Person('Andy', 12)
    person.play()
    person.age = 22
    person.play()

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190924/20121116EF0bTXvtDC.png

__ slots __

動態語言允許我們在程序運行時給 object 綁定新的屬性或 method,也可以對已經綁定的屬性和 method 解綁定。
如果需要限定自定義類型的 object 只能綁定某些屬性,可以利用 __ slots __ 變數來進行限定。 __ slots __ 的限定只對當前 class 的 object 生效,對子類別並不作用。

class Person(object):

    # 限定 Person object 只能绑定 _name, _age 和 _gender 屬性
    __slots__ = ('_name', '_age', '_gender')

    def __init__ (self, name, age):
        self._name = name
        self._age = age

    @property
    def name(self):
        return self._name

    @property
    def age(self):
        return self._age

    @age.setter
    def age (self, age):
        self._age = age

    def play(self):
        if self._age <= 16:
            print('%s 正在玩手機' % self._name)
        else:
            print('%s 正在玩電腦' % self._name)

def main():
    person = Person('Andy', 22)
    person.play()
    person._gender = '男'
    # person._is_gay = True    沒有在 __slots__ 中就會得到錯誤
    # AttributeError: 'Person' object has no attribute '_is_gay'
    
if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190924/20121116FBGgDdVmF9.png

靜態 method 和 類別 (class) method

在 class 中的 method 並不需要都是物件 (object) method。
例如定義一個'三角形'class,透過傳入三條邊長來構成三角形,並提供計算周長和面積 method。
但傳入的三條邊長未必能構造出三角形 object,因此先寫一個 method 來驗證三條邊長是否可以構成三角形。
在調用這個 method 時三角形 object 尚未創建,所以這個 method 是屬於三角形 class 而並不屬於三角形 object 。可以使用靜態 method 來解決這類問題。

from math import sqrt

class Triangle(object):

    def __init__ (self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    # 使用 @staticmethod 不需要實例化,直接使用 class.method() 名來調用
    @staticmethod           
    def is_valid (a, b, c):
        return a + b > c and b + c > a and a + c > b

    def perimeter(self):
        return self._a + self._b + self._c

    def area(self):
        half = self.perimeter() / 2
        return sqrt(half * (half - self._a) *
                    (half - self._b) * (half - self._c))

def main():
    a, b, c = 3, 4, 5
    # 靜態 method 和 class method 都是透過给 class 發消息來調用
    if Triangle.is_valid (a, b, c):
        t = Triangle (a, b, c)
        # 也可以透過給 class 發消息來調用 object method,
        # 但是要傳入接收消息的 object 作为參數
        print(Triangle.perimeter(t))
        print(Triangle.area(t))
        print(t.perimeter())
        print(t.area())    
    else:
        print('無法構成三角形.')

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190924/201211160jiXMH7PcD.png
Python 還可以在 class 中定義 class method,class method 的第一個參數約定名為 cls,它代表的是當前 class 相關的信息的 object(class 本身也是一個 object,有的地方也稱之為 class 的元數據 object)通過這個參數我們可以獲取和 class 相關的信息並且可以創建出 class 的 object。

from time import time, localtime, sleep

class Clock(object):

    def __init__(self, hour = 0, minute = 0, second = 0):
        self._hour = hour
        self._minute = minute
        self._second = second

    # 使用 @ classmethod 不需要實例化,直接使用 class.method() 名來調用
    @ classmethod
    def now(cls):
        ctime = localtime(time()) # localtime()函數,作用是格式化時間為本地的時間 
        return cls(ctime.tm_hour, ctime.tm_min, ctime.tm_sec)

    def run(self):
        self._second += 1
        if self._second == 60:
            self._second = 0
            self._minute += 1
            if self._minute == 60:
                self._minute = 0
                self._hour += 1
                if self._hour == 24:
                    self._hour = 0

    def show(self):
        return '%02d:%02d:%02d' % \
               (self._hour, self._minute, self._second)

def main():
    # 透過 class method 創建 object 並獲取系統時間
    clock = Clock.now()
    while True:
        print(clock.show())
        sleep(1)
        clock.run()

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190924/20121116I6TJUEZNXA.png
time 模組中,可以使用 索引和屬性訪問 time.struct_time object 的值。
https://ithelp.ithome.com.tw/upload/images/20190924/20121116QNwm7Sq4be.pnghttps://ithelp.ithome.com.tw/upload/images/20190924/20121116v9d3IsNvCb.png

class 之間的關係

簡單的說,類和類之間的關係有三種:is-a、has-a 和 use-a 關係。

  • is-a 關係也叫繼承或泛化,比如學生和人的關係、手機和電子產品的關係屬於繼承關係。
  • has-a 關係通常稱之為關聯,比如部門和員工的關係,汽車和引擎的關係都屬於關聯關係。關聯關係如果是整體和部分的關聯,那麼我們稱之為聚合關係。如果整體進一步負責了部分的生命週期(整體和部分是不可分割的,同時同在也同時消亡)那麼這種就是最強的關聯關係,我們稱之為合成關係。
  • use-a 關係通常稱之為依賴,比如司機有一個駕駛的行為 (method),其中的參數使用到了汽車,那麼司機和汽車的關係就是依賴關係。

繼承 (inheritance) 和多型 (polymorphism)

可以在已有 class 的基礎上創建新 class ,這其中的一種做法就是讓一個 class 從另一個 class 那將屬性和 method 直接繼承下來。提供繼承信息的我們稱之為父類別 (class),得到繼承信息的我們稱之為子類別 (class)。
子類別 (class) 除了繼承父類別 (class) 提供的屬性和 method,還可以定義自己特有的屬性和 method,所以子類別 (class) 比父類別 (class) 擁有的更多的能力。

class Person(object):      # 父類別 (class) 人

    def __init__ (self, name, age):
        self._name = name
        self._age = age

    @property
    def name(self):
        return self._name

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, age):
        self._age = age

    def play(self):
        print('%s正在愉快的玩耍' % self._name)

    def watch_movie(self):
        if self._age >= 18:
            print('%s 能觀看所有電影' % self._name)
        else:
            print('%s 只能觀看普通電影' % self._name)

class Student(Person):      # 子類別 (class) 學生

    def __init__ (self, name, age, grade):
        super().__init__ (name, age)  # 繼承父類別 (class)
        self._grade = grade

    @property
    def grade(self):
        return self._grade

    @grade.setter
    def grade (self, grade):
        self._grade = grade

    def study (self, course):
        print('%s的 %s 正在學習%s' % (self._grade, self._name, course))

class Teacher(Person):      # 子類別 (class) 老師

    def __init__ (self, name, age, title):
        super().__init__ (name, age)  # 繼承父類別 (class)
        self._title = title

    @property
    def title(self):
        return self._title

    @title.setter
    def title (self, title):
        self._title = title

    def teach (self, course):
        print('%s %s正在講 %s' % (self._name, self._title, course))

def main():
    stu = Student('Andy', 15, '國三')
    stu.study('數學')
    stu.watch_movie()
    t = Teacher('Alan', 38, '老師')
    t.teach('Python 程式設計')
    t.watch_movie()

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190924/20121116PQg8x72Dck.png
子類別 (class) 繼承父類別 (class) 的 method 後,可以對父類別 (class) 已有的 method 給出新的版本,這個動作稱之為方法 (method) 覆寫 (override)。
透過方法 (method) 覆寫 (override) 我們可以讓父類別 (class) 的同一個行為在子類別 (class) 中擁有不同的實現版本,當我們調用這個經過子類別 (class) 覆寫 (override) 的 method 時,不同的子類別 (class) object 會表現出不同的行為,這個就是多型 (poly-morphism)。

將 Pet class 處理成了一個抽象 class,所謂抽象 class 就是不能夠創建 object 的 class,這種 class 就是專門讓其他 class 去繼承它。
利用 abc 模組的 ABCMeta 元類別 (class) 和 abstractmethod 包裝器來達到抽象 class 的效果,如果一個 class 中存在抽象 method 那麼這個 class 就不能夠實例化(創建 object)。
Dog 和 Cat兩個子類別 (class) 分別對 Pet class 中的 make_voice 抽象方法進行覆寫 (override) 並給出了不同的實現版本,當我們在 main 函數中調用該 method 時,這個 method 就表現出多型 (polymorphism),也就是同樣的 method 做了不同的事情。

from abc import ABCMeta, abstractmethod

class Pet (object, metaclass = ABCMeta):  # 抽象父類別 (class) 寵物

    def __init__ (self, nickname):
        self._nickname = nickname

    @abstractmethod
    def make_voice(self):
        pass

class Dog(Pet):         # 子類別 (class) 狗

    def make_voice(self):   # 繼承抽象父類別 (class) 並覆寫
        print('%s : 汪汪汪...' % self._nickname)

class Cat(Pet):         # 子類別 (class) 貓

    def make_voice(self):   # 繼承抽象父類別 (class) 並覆寫
        print('%s : 喵...喵...' % self._nickname)

def main():
    pets = [Dog('小黑'), Cat('小喵'), Dog('小白')]
    for pet in pets:
        pet.make_voice()

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190924/201211165Td04E3AuS.png

練習

練習1 - 超人打怪獸

from abc import ABCMeta, abstractmethod
from random import randint, randrange

class Fighter (object, metaclass = ABCMeta): # 抽象父類別 (class) 攻擊
  
    # 利用__slots__ 限定 object 可以绑定的成員變數
    __slots__ = ('_name', '_hp')

    def __init__ (self, name, hp): # 初始化(名字, 生命值)
        self._name = name
        self._hp = hp

    @property
    def name(self):
        return self._name

    @property
    def hp(self):
        return self._hp

    @hp.setter
    def hp(self, hp):
        self._hp = hp if hp >= 0 else 0

    @property
    def alive(self):
        return self._hp > 0

    @abstractmethod
    def attack(self, other):    # 攻擊(對象)
        pass

class Ultraman(Fighter):    # 子類別 (class) 超人

    __slots__ = ('_name', '_hp', '_mp')

    def __init__ (self, name, hp, mp): # 初始化(名字, 生命值, 魔法值)
        super().__init__ (name, hp)    # 繼承抽象父類別 (class) 
        self._mp = mp

    def attack (self, other):          # 繼承抽象父類別 (class) 並覆寫
        other.hp -= randint(15, 25)

    def huge_attack (self, other):     # 必殺攻擊(對象)
        if self._mp >= 50:
            self._mp -= 50
            injury = other.hp * 3 // 4
            injury = injury if injury >= 50 else 50
            other.hp -= injury
            return True                # 成功
        else:
            self.attack(other)
            return False

    def magic_attack (self, others):   # 魔法攻擊(對象)
        if self._mp >= 20:
            self._mp -= 20
            for temp in others:
                if temp.alive:
                    temp.hp -= randint(10, 15)
            return True
        else:
            return False

    def resume(self):                   # 恢復魔法
        incr_point = randint(1, 10)
        self._mp += incr_point
        return incr_point

    def __str__(self):
        return '~~~ %s 超人 ~~~\n' % self._name + \
            '生命值: %d\n' % self._hp + \
            '魔法值: %d\n' % self._mp

class Monster(Fighter):             # 子類別 (class) 怪獸

    __slots__ = ('_name', '_hp')

    def attack(self, other):        # 繼承抽象父類別 (class) 並覆寫
        other.hp -= randint(10, 20)

    def __str__(self):
        return '~~~ %s 怪獸 ~~~\n' % self._name + \
            '生命值: %d\n' % self._hp

def is_any_alive(monsters):         # 判斷有没有怪獸活著
    for monster in monsters:
        if monster.alive > 0:
            return True
    return False

def select_alive_one(monsters):     # 選中一隻活著的怪獸
    monsters_len = len(monsters)
    while True:
        index = randrange(monsters_len)
        monster = monsters[index]
        if monster.alive > 0:
            return monster

def display_info(ultraman, monsters): # 顯示訊息
    print(ultraman)
    for monster in monsters:
        print(monster, end='')

def main():
    u = Ultraman('Andy', 1000, 120)
    m1 = Monster('monsterA', 250)
    m2 = Monster('monsterB', 500)
    m3 = Monster('monsterC', 750)
    ms = [m1, m2, m3]
    fight_round = 1
    while u.alive and is_any_alive(ms):
        print('======== 第 %02d 回合 ========' % fight_round)
        m = select_alive_one(ms)    # 選中一隻活著的怪獸
        skill = randint(1, 10)      # 隨機選擇使用技能
        if skill <= 6:              # 60% 的機率使用普通攻擊
            print('%s 使用普通攻击打了 %s' % (u.name, m.name))
            u.attack(m)
            print('%s 的魔法值恢復了 %d 點' % (u.name, u.resume()))
        elif skill <= 9:  # 30% 的機率使用魔法攻擊(可能因魔法值不足而失敗)
            if u.magic_attack(ms):
                print('%s 使用了魔法攻擊' % u.name)
            else:
                print('%s 使用魔法失敗' % u.name)
        else:  # 10% 的機率使用必殺攻擊(如果魔法值不足則使用普通攻擊)
            if u.huge_attack(m):
                print('%s 使用必殺攻擊打了 %s' % (u.name, m.name))
            else:
                print('%s 使用普通攻擊打了 %s' % (u.name, m.name))
                print('%s 的魔法值恢復了 %d 點' % (u.name, u.resume()))
        if m.alive > 0:  # 如果怪獸没有死就回擊超人
            print('%s 回擊 %s' % (m.name, u.name))
            m.attack(u)
        display_info(u, ms)  # 每個回合結束顯示訊息
        fight_round += 1
    print('\n======== 戰鬥结束! ========\n')
    if u.alive > 0:
        print('%s 超人勝利!' % u.name)
    else:
        print('怪獸勝利!')

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190924/20121116ssf14262Nr.png https://ithelp.ithome.com.tw/upload/images/20190924/20121116eFwYiDJhQs.png
這個練習很特別,當中有很多樂趣,利用隨機的值,大戰了 66 回合。

練習2 - 撲克牌遊戲

import random

class Card(object):         # 一張牌

    def __init__(self, suite, face): # 初始化(花色,數字)
        self._suite = suite
        self._face = face

    @property
    def face(self):
        return self._face

    @property
    def suite(self):
        return self._suite

    def __str__(self):
        if self._face == 1:
            face_str = 'A'
        elif self._face == 11:
            face_str = 'J'
        elif self._face == 12:
            face_str = 'Q'
        elif self._face == 13:
            face_str = 'K'
        else:
            face_str = str(self._face)
        return '%s%s' % (self._suite, face_str)
    
    def __repr__(self):
        return self.__str__()

class Poker(object):    # 一副牌

    def __init__(self):
        self._cards = [Card(suite, face) 
                       for suite in '♠♥♣♦'
                       for face in range(1, 14)]
        self._current = 0

    @property
    def cards(self):
        return self._cards

    def shuffle(self):  # 洗牌(隨機)
        self._current = 0
        random.shuffle(self._cards)

    @property
    def next(self):     # 發牌
        card = self._cards[self._current]
        self._current += 1
        return card

    @property
    def has_next(self): # 還有沒有牌
        return self._current < len(self._cards)

class Player(object):   # 玩家

    def __init__(self, name):
        self._name = name
        self._cards_on_hand = []

    @property
    def name(self):
        return self._name

    @property
    def cards_on_hand(self):
        return self._cards_on_hand

    def get(self, card):         # 拿牌
        self._cards_on_hand.append(card)

    def arrange(self, card_key):  # 整理牌
        self._cards_on_hand.sort(key = card_key)

# 排序規則 - 先根據花色再根據大小排序
def get_key(card):
    return (card.suite, card.face)

def main():
    p = Poker()
    p.shuffle()
    players = [Player('pA'), Player('pB'), Player('pC'), Player('pD')]
    for _ in range(13):
        for player in players:
            player.get(p.next)
    for player in players:
        print(player.name + ' :', end =' ')
        player.arrange(get_key)
        print(player.cards_on_hand)

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190924/20121116Npju2haSfy.png
練習3 - 工資结算系统
某公司有是部門經理、程序員和銷售員,根據提供的員工訊息來計算月薪。
部門經理的月薪是每月固定 45000 元
程序員的月薪按本月工作時間計算每小時 200 元
銷售員的月薪是 22000 元的底薪加上銷售額 5% 的加成

from abc import ABCMeta, abstractmethod

class Employee (object, metaclass = ABCMeta):   # 抽象父類別 (class) 員工

    def __init__ (self, name): 
        self._name = name

    @property
    def name(self):
        return self._name

    @abstractmethod
    def get_salary(self):       # 月薪
        pass

class Manager(Employee):        # 子類別 (class) 部門經理

    def get_salary(self):       # 繼承抽象父類別 (class) 並覆寫
        return 45000.0

class Programmer(Employee):     # 子類別 (class) 程序員
    
    def __init__(self, name, working_hour = 0):
        super().__init__(name)  # 繼承抽象父類別 (class) 
        self._working_hour = working_hour

    @property
    def working_hour(self):
        return self._working_hour

    @working_hour.setter
    def working_hour(self, working_hour):
        self._working_hour = working_hour if working_hour > 0 else 0

    def get_salary(self):       # 繼承抽象父類別 (class) 並覆寫
        return 200.0 * self._working_hour

class Salesman(Employee):       # 子類別 (class) 銷售員
    
    def __init__(self, name, sales = 0):
        super().__init__(name)  # 繼承抽象父類別 (class)
        self._sales = sales

    @property
    def sales(self):
        return self._sales

    @sales.setter
    def sales(self, sales):
        self._sales = sales if sales > 0 else 0

    def get_salary(self):      # 繼承抽象父類別 (class) 並覆寫
        return 22000.0 + self._sales * 0.05

def main():
    emps = [
        Manager('Alan'), Programmer('Andy'),
        Manager('Adam'), Salesman('Alex'),
        Salesman('Alice'), Programmer('Angle'),
        Programmer('Amy')
    ]
    for emp in emps:
        if isinstance(emp, Programmer):
            emp.working_hour = int(input('請輸入 %s 本月工作時數: ' % emp.name))
        elif isinstance(emp, Salesman):
            emp.sales = float(input('請輸入 %s 本月銷售額: ' % emp.name))
        # 同樣是 get_salary 但不同的職位表現出不同的行為(多型)
        print('%s 本月工資為: %s 元' %
              (emp.name, emp.get_salary()))

if __name__ == '__main__':
    main()

https://ithelp.ithome.com.tw/upload/images/20190924/20121116pvJpu14gkN.png


上一篇
[Day08] 物件導向編程基礎
下一篇
[Day10] 圖形使用者介面 (GUI) 和遊戲開發
系列文
30天學python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言